]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
extensionUtils: Add InjectionManager
authorFlorian Müllner <fmuellner@gnome.org>
Fri, 23 Jun 2023 17:53:05 +0000 (19:53 +0200)
committerFlorian Müllner <fmuellner@gnome.org>
Sat, 5 Aug 2023 10:34:41 +0000 (12:34 +0200)
It is fairly common for extensions to monkey-patch existing
classes. Add a small helper class that makes this a tad bit
more convenient.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2809>

js/extensions/extension.js
tests/meson.build
tests/unit/injectionManager.js [new file with mode: 0644]

index 2e06b3dfeb05f1751699230a09baeaa836619663..a0d3f049dd55689939db8d3574c5a82a42689439 100644 (file)
@@ -29,3 +29,71 @@ export class Extension extends ExtensionBase {
 export const {
     gettext, ngettext, pgettext,
 } = Extension.defineTranslationFunctions();
+
+export class InjectionManager {
+    #savedMethods = new Map();
+
+    /**
+     * @callback CreateOverrideFunc
+     * @param {Function?} originalMethod - the original method if it exists
+     * @returns {Function} - a function to be used as override
+     */
+
+    /**
+     * Modify, replace or inject a method
+     *
+     * @param {object} prototype - the object (or prototype) that is modified
+     * @param {string} methodName - the name of the overwritten method
+     * @param {CreateOverrideFunc} createOverrideFunc - function to call to create the override
+     */
+    overrideMethod(prototype, methodName, createOverrideFunc) {
+        const originalMethod = this._saveMethod(prototype, methodName);
+        prototype[methodName] = createOverrideFunc(originalMethod);
+    }
+
+    /**
+     * Restore the original method
+     *
+     * @param {object} prototype - the object (or prototype) that is modified
+     * @param {string} methodName - the name of the method to restore
+     */
+    restoreMethod(prototype, methodName) {
+        const savedProtoMethods = this.#savedMethods.get(prototype);
+        if (!savedProtoMethods)
+            return;
+
+        const originalMethod = savedProtoMethods.get(methodName);
+        if (originalMethod === undefined)
+            delete prototype[methodName];
+        else
+            prototype[methodName] = originalMethod;
+
+        savedProtoMethods.delete(methodName);
+        if (savedProtoMethods.size === 0)
+            this.#savedMethods.delete(prototype);
+    }
+
+    /**
+     * Restore all original methods and clear overrides
+     */
+    clear() {
+        for (const [proto, map] of this.#savedMethods) {
+            map.forEach(
+                (_, methodName) => this.restoreMethod(proto, methodName));
+        }
+        console.assert(this.#savedMethods.size === 0,
+            `${this.#savedMethods.size} overrides left after clear()`);
+    }
+
+    _saveMethod(prototype, methodName) {
+        let savedProtoMethods = this.#savedMethods.get(prototype);
+        if (!savedProtoMethods) {
+            savedProtoMethods = new Map();
+            this.#savedMethods.set(prototype, savedProtoMethods);
+        }
+
+        const originalMethod = prototype[methodName];
+        savedProtoMethods.set(methodName, originalMethod);
+        return originalMethod;
+    }
+}
index df35080fe296a249be3475e3e5fd4a1ef1807516..a22a0d949f2ccf4934e6f4c92347382a821ef790 100644 (file)
@@ -20,6 +20,7 @@ dbus_runner = configure_file(
 
 unit_tests = [
     'highlighter',
+    'injectionManager',
     'insertSorted',
     'jsParse',
     'markup',
diff --git a/tests/unit/injectionManager.js b/tests/unit/injectionManager.js
new file mode 100644 (file)
index 0000000..6de5094
--- /dev/null
@@ -0,0 +1,93 @@
+const JsUnit = imports.jsUnit;
+
+import '../../js/ui/environment.js';
+
+import {InjectionManager} from '../../js/extensions/extension.js';
+
+class Object1 {
+    count = 0;
+
+    getNumber() {
+        return 42;
+    }
+
+    getCount() {
+        return ++this.count;
+    }
+}
+
+/**
+ * @param {object} object to modify
+ */
+function addInjections(object) {
+    // extend original method
+    injectionManager.overrideMethod(
+        object, 'getNumber', originalMethod => {
+            return function () {
+                // eslint-disable-next-line no-invalid-this
+                const num = originalMethod.call(this);
+                return 2 * num;
+            };
+        });
+
+    // override original method
+    injectionManager.overrideMethod(
+        object, 'getCount', () => {
+            return function () {
+                return 42;
+            };
+        });
+
+    // inject new method
+    injectionManager.overrideMethod(
+        object, 'getOtherNumber', () => {
+            return function () {
+                return 42;
+            };
+        });
+}
+
+
+const injectionManager = new InjectionManager();
+let obj;
+
+// Prototype injections
+addInjections(Object1.prototype);
+
+obj = new Object1();
+
+// new obj is modified
+JsUnit.assertEquals(obj.getNumber(), 84);
+JsUnit.assertEquals(obj.getCount(), 42);
+JsUnit.assertEquals(obj.count, 0);
+JsUnit.assertEquals(obj.getOtherNumber(), 42);
+
+injectionManager.clear();
+
+obj = new Object1();
+
+// new obj is unmodified
+JsUnit.assertEquals(obj.getNumber(), 42);
+JsUnit.assertEquals(obj.getCount(), obj.count);
+JsUnit.assert(obj.count > 0);
+JsUnit.assertRaises(() => obj.getOtherNumber());
+
+// instance injections
+addInjections(obj);
+
+// obj is now modified
+JsUnit.assertEquals(obj.getNumber(), 84);
+JsUnit.assertEquals(obj.getCount(), 42);
+JsUnit.assertEquals(obj.count, 1);
+JsUnit.assertEquals(obj.getOtherNumber(), 42);
+
+injectionManager.restoreMethod(obj, 'getNumber');
+JsUnit.assertEquals(obj.getNumber(), 42);
+
+injectionManager.clear();
+
+// obj is unmodified again
+JsUnit.assertEquals(obj.getNumber(), 42);
+JsUnit.assertEquals(obj.getCount(), obj.count);
+JsUnit.assert(obj.count > 0);
+JsUnit.assertRaises(() => obj.getOtherNumber());